﻿using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;

namespace SBRepository.LinqToSql
{
    public class LinqToSqlRepository<TEntity> : IRepository<TEntity> where TEntity : class, new()
    {
        /// <summary>
        /// Session to use.
        /// </summary>
        private ISession session;

        /// <summary>
        /// The table containing entities of type <typeparam name="TEntity"/>.
        /// </summary>
        private Table<TEntity> table;

        /// <summary>
        /// The table name.
        /// </summary>
        private string tableName;

        /// <summary>
        /// Key column name.
        /// </summary>
        private string key;

        /// <summary>
        /// Initializes a new instance of the <see cref="LinqToSqlRepository{TEntity}"/> class.
        /// </summary>
        /// <param name="session">The session to use.</param>
        public LinqToSqlRepository(ISession session)
        {
            if (session == null)
            {
                throw new ArgumentNullException("session");
            }

            Session = session;
        }

        /// <summary>
        /// Gets the current session.
        /// </summary>
        /// <value>The session.</value>
        public ISession Session
        {
            get
            {
                return session;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }

                session = value;
                var context = ((LinqToSqlSession)session).Context;
                var metaTable = context.Mapping.GetTable(typeof(TEntity));
                var identities = metaTable.RowType.IdentityMembers;
                if (identities.Count > 0)
                {
                    key = identities[0].Name;
                }

                table = context.GetTable<TEntity>();
                tableName = metaTable.TableName;
            }
        }

        /// <summary>
        /// Gets the entity by specified ID.
        /// </summary>
        /// <param name="id">The value of ID.</param>
        /// <returns>Entity.</returns>
        public TEntity GetById(object id)
        {
            if (String.IsNullOrEmpty(key))
            {
                throw new MethodAccessException("Specified entity doesn't have associated key");
            }

            if (id == null)
            {
                throw new ArgumentNullException("id");
            }

            var t = Expression.Parameter(typeof(TEntity), "t");
            var predicate = (Expression<Func<TEntity, bool>>)Expression.Lambda(Expression.Equal(Expression.Property(t, key), Expression.Constant(id)), t);

            return QueryAll().Where(predicate).SingleOrDefault() ?? new TEntity();
        }

        /// <summary>
        /// Gets all entities from the specified data source.
        /// </summary>
        /// <returns>Entities.</returns>
        public IEnumerable<TEntity> GetAll()
        {
            return QueryAll().ToList();
        }

        /// <summary>
        /// Gets all entities from the specified data source.
        /// </summary>
        /// <param name="predicate">The predicate to filter result.</param>
        /// <returns>Entities.</returns>
        public IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate)
        {
            if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            return QueryAll().Where(predicate).ToList();
        }

        /// <summary>
        /// Gets the query that returns all entities from the specified data store.
        /// </summary>
        /// <returns>The query.</returns>
        public IQueryable<TEntity> QueryAll()
        {
            return table;
        }

        /// <summary>
        /// Specifies what additional entities include while loading main entity.
        /// </summary>
        /// <param name="includes">Additional entities to include.</param>
        /// <returns>The query.</returns>
        public void Include(params Expression<Func<TEntity, object>>[] includes)
        {
            var loadOptions = new DataLoadOptions();
            foreach (var include in includes)
            {
                loadOptions.LoadWith(include);
            }

            table.Context.LoadOptions = loadOptions;
        }

        /// <summary>
        /// Determines whether entity is new.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns>
        /// <c>True</c> if entity is new; otherwise, <c>false</c>.
        /// </returns>
        public bool IsNew(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            bool isNew;

            if (String.IsNullOrEmpty(key))
            {
                isNew = !table.Contains(entity);
            }
            else
            {
                var value = entity.GetType().GetProperty(key).GetValue(entity, null);

                isNew = value.GetType() == typeof(Guid) ? (Guid)value == Guid.Empty : Convert.ToInt64(value) == 0;
            }

            return isNew;
        }

        /// <summary>
        /// Inserts the specified entity to the underlying data source.
        /// </summary>
        /// <param name="entity">Entity to insert.</param>
        public void Insert(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            table.InsertOnSubmit(entity);
        }

        /// <summary>
        /// Inserts all entities to the underlying data source.
        /// </summary>
        /// <param name="entities">Entities to insert.</param>
        public void InsertAll(IEnumerable<TEntity> entities)
        {
            if (entities == null)
            {
                throw new ArgumentNullException("entities");
            }

            table.InsertAllOnSubmit(entities);
        }

        /// <summary>
        /// Updates the specified entity to the underlying data source.
        /// </summary>
        /// <param name="entity">Entity to update.</param>
        public void Update(TEntity entity)
        {
            Update(entity, false);
        }

        /// <summary>
        /// Updates the specified entity.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="isDetached">If set to <c>true</c>, entity is detached and need to be attached.</param>
        public void Update(TEntity entity, bool isDetached)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            if (isDetached)
            {
                entity = table.Detach(entity);
                table.Attach(entity, true);
            }
            else
            {
                // do nothing
            }
        }

        /// <summary>
        /// Updates all entities to the underlying data source.
        /// </summary>
        /// <param name="entities">Entities to update.</param>
        public void UpdateAll(IEnumerable<TEntity> entities)
        {
            if (entities == null)
            {
                throw new ArgumentNullException("entities");
            }

            // do nothing
        }

        /// <summary>
        /// Deletes the specified entity from the underlying data source.
        /// </summary>
        /// <param name="entity">Entity to delete.</param>
        public void Delete(TEntity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            table.DeleteOnSubmit(entity);

        }

        /// <summary>
        /// Deletes all entities from the underlying data source.
        /// </summary>
        /// <param name="entities">Entities to delete.</param>
        public void DeleteAll(IEnumerable<TEntity> entities)
        {
            if (entities == null)
            {
                throw new ArgumentNullException("entities");
            }

            table.DeleteAllOnSubmit(entities);

        }

        /// <summary>
        /// Deletes all entities from the underlying data source.
        /// </summary>
        public void DeleteAll()
        {
            var sql = "DELETE FROM " + tableName;
            table.Context.ExecuteCommand(sql);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Session.Dispose();
        }
    }
}
